/**
*    Copyright (C) 2008  Chase Kernan 
*    chase.kernan@gmail.com
*
*    This program is free software: you can redistribute it and/or modify
*    it under the terms of the GNU General Public License as published by
*    the Free Software Foundation, either version 3 of the License, or
*    (at your option) any later version.
*
*    This program is distributed in the hope that it will be useful,
*    but WITHOUT ANY WARRANTY; without even the implied warranty of
*    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
*    GNU General Public License for more details.
*
*    You should have received a copy of the GNU General Public License
*    along with this program.  If not, see <http://www.gnu.org/licenses/>.
* 
**/

package com.chasekernan.hxnova.core.stars;

import haxe.xml.Check;
import com.chasekernan.hxnova.utils.Utils;

/**
    Contains the mineral concentrations of a star system (held in a float array).
**/
class Concentrations {
    
    /**
        The basic mineral types as strings that is used throughout.
    **/
    public static var MINERAL_TYPES : Array<String> = ["ironium", "boranium", 
                                                       "germanium"];
    
    /**
        The minimum value of concentrations (1.0 in Stars!)
    **/
    public static var MIN_VALUE : Float = 1.0;
    
    /**
        The maximum value of a concentration.
        As far as I know, the max is 200 in Stars! but above 100 is reached 
        mainly through coment strikes (I think...).
    **/
    public static var MAX_VALUE : Float = 200.0;
    
    /**
        Homeworlds have a minimum concentration of 30.0.
    **/
    public static var MIN_HW_VALUE : Float = 30.0;
    
    /**
        The actual concentration values, indexed by their position in the 
        mineral type array.
    **/
    public var values : Array<Float>;
    
    /**
        Whether or not these concentrations are for a homeworld.
        TODO: Perhaps allow the star system to keep track of this?
    **/
    public var isHomeworld : Null<Bool>;
    
    /**
        If isHomeworld is not supplied, its assumed to be false, but still 
        recorded as null.
        Also, the concentrations are set to random values.
    **/
    public function new(?isHomeworld : Bool) {
        this.isHomeworld = isHomeworld;
        values = new Array<Float>();
        
        for(i in MINERAL_TYPES) values.push(getRandomValue());
    }
    
    //TODO: Use actual formula for concentrations value.
    private function getRandomValue() : Float {
        var min = if (isHomeworld != null && isHomeworld) MIN_HW_VALUE else 
                MIN_VALUE;
        return Math.random() * (100.0 - min) + min; //use 100.0 for now
    }
    
    /**
        Returns the concentration based on the mineral type.
        Throws an error if there's no such type.
    **/
    public function getValue(type : String) : Float {
        for(i in 0...MINERAL_TYPES.length) {
            if(MINERAL_TYPES[i] == type) return values[i];
        }
        
        throw "There is no such mineral type as " + type;
    }
    
    /**
        Sets a concentration to the given value.
        Throws an error if the value is not with in bounds or there
        is no such mineral type.
    **/
    public function setValue(type : String, value : Float) {
        if(!checkValue(value)) 
            throw "Given concentration value is out of bounds: " + value;
        
        for(i in 0...MINERAL_TYPES.length) {
            if(MINERAL_TYPES[i] == type) {
                values[i] = value;
                return;
            }
        }
        
        throw "There is no such mineral type as " + type;
    }
    
    public function checkValue(value : Float) : Bool{
        if(isHomeworld != null && isHomeworld) 
            return (value >= MIN_HW_VALUE && value <= MAX_VALUE);
        else
            return (value >= MIN_VALUE && value <= MAX_VALUE);
    }
    
    public function iterator () {
        return values.iterator();
    }
    
    
//    public static function getXmlRules() : Rule {
//        var nameArray : Array<Rule> = new Array<Rule>();
//        
//        for(i in MINERAL_TYPES) nameArray.push(Rule.RNode(i, [Att('value')]));
//        
//        return Rule.RNode(XML_TITLE, [Att('is_homeworld')], 
//                   Rule.RList(nameArray, false));
//    }
    
    public static var XML_TITLE = "mineral_conc";
    
    /**
        Writes a concentrations to xml.
        Sample XML:
            <mineral_conc is_homeworld="false">
                <ironium value="45.0">
                <boranium value="67.64532">
                <germanium value="42.1023">
            </mineral_conc>
    **/        
    public static function writeToXml(c : Concentrations) : Xml {
        var xml : Xml = Xml.parse('<' + XML_TITLE + '></' + XML_TITLE + '>')
                            .firstElement();
        
        if(c.isHomeworld != null) xml.set(
                'is_homeworld', Std.string(c.isHomeworld));
        
        for(i in MINERAL_TYPES) {
            var childXml = Xml.createElement(i);
            childXml.set('value', Std.string(c.getValue(i)));
            xml.addChild(childXml);
        }
        //check it 
//        Check.checkNode(xml, getXmlRules());
        return xml;
    }
    
    /**
        Reads a concentrations file and throws an error if there are any 
        problems.
    **/
    public static function readFromXml(xml : Xml) : Concentrations {
        try {
            
            //check type
 //           Check.checkNode(xml, getXmlRules());
            
            var isHomeworld : Null<Bool> = null;
            if (Utils.xmlHasElement(xml, "is_homeworld"))
                isHomeworld = xml.firstElement().get('is_homeworld') == 'true';
            
            var c = new Concentrations(isHomeworld);
            
            for(i in xml.elements()) 
                c.setValue(i.nodeName, Std.parseFloat(i.get('value')));
                
            return c;
        } catch (e : Dynamic) {
            throw "Bad mineral concentration file: " + e;
        }
    }
    
    public function clone() : Concentrations {
        var c : Concentrations = new Concentrations(isHomeworld);
        c.values = values.copy();
        return c;
    }
}

#if unit
import haxe.unit.TestCase;

class ConcentrationsTest extends haxe.unit.TestCase {
    public var conc : Concentrations;
    
    public function testConcentrations() {
        
        for (i in 0...100) {
            conc = new Concentrations(true);
            for (value in conc) {
                assertTrue(value >= Concentrations.MIN_HW_VALUE);
            }
        
            conc = new Concentrations();
            assertEquals(conc.isHomeworld, null);
            for (value in conc) {
                assertTrue(value >= Concentrations.MIN_VALUE);
                assertTrue(value <= Concentrations.MAX_VALUE);
            }
            
            conc = new Concentrations(false);
            for (value in conc) {
                assertTrue(value >= Concentrations.MIN_VALUE);
                assertTrue(value <= Concentrations.MAX_VALUE);
            }
        }
        
        conc = new Concentrations();
        
        try {
            conc.getValue("gibberish");
            assertTrue(false);
        } catch (e : Dynamic) {
            assertTrue(true);
        }
        
        var value = conc.getValue(Concentrations.MINERAL_TYPES[0]);
        assertTrue(value >= Concentrations.MIN_VALUE);
        assertTrue(value <= Concentrations.MAX_VALUE);
        
        try {
            conc.setValue("gibberish", 12.2);
            assertTrue(false);
        } catch (e : Dynamic) {
            assertTrue(true);
        }
        
        try {
            conc.setValue(Concentrations.MINERAL_TYPES[1], -12.3);
            assertTrue(false);
        } catch (e : Dynamic) {
            assertTrue(true);
        }
        
        conc.setValue(Concentrations.MINERAL_TYPES[2], 45.3);
        assertTrue(Math.abs(
                conc.getValue(Concentrations.MINERAL_TYPES[2]) - 45.3) 
                < 0.001);
    }
    
    public function testConcentrationsXml() {
        conc = new Concentrations(true);
        
        var xml = Concentrations.writeToXml(conc);
        for (element in xml.elements()) {
            assertTrue(Math.abs(conc.getValue(element.nodeName) - 
                    Std.parseFloat(element.get("value"))) < 0.001);
        }
        
        assertEquals(xml.get("is_homeworld"), "true");
        
        conc = new Concentrations();
        assertFalse(Concentrations.writeToXml(conc).exists("is_homeworld"));
        
        conc = new Concentrations(false);
        
        var readValues = Concentrations.readFromXml(
                Concentrations.writeToXml(conc));
        for (minType in Concentrations.MINERAL_TYPES) {
            assertTrue(Math.abs(
                    conc.getValue(minType) - readValues.getValue(minType))
                    < 0.01);
        }
        
        assertFalse(readValues.isHomeworld);
    }
}

#end